/*
 * JSF Toolkit Component Framework
 * Copyright (C) 2007 Noah Sloan <iamnoah A-T gmail D0T com>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 */
package com.jsftoolkit.gen.writer.impl;

import java.io.PrintStream;
import java.lang.reflect.Array;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.Map.Entry;

import javax.faces.context.FacesContext;
import javax.faces.event.FacesEvent;
import javax.faces.event.FacesListener;

import com.jsftoolkit.ajax.AjaxContext;
import com.jsftoolkit.base.ComponentHelp;
import com.jsftoolkit.gen.writer.ClassWriter;
import com.jsftoolkit.gen.writer.ClassWriterWrapper;
import com.jsftoolkit.gen.writer.ComponentWriter;
import com.jsftoolkit.utils.ClassUtils;
import com.jsftoolkit.utils.Utils;

public class ComponentWriterImpl extends ClassWriterWrapper implements
		ComponentWriter {

	private LinkedHashMap<String, Class<?>> stateToSave = new LinkedHashMap<String, Class<?>>();

	/**
	 * Initializes without a {@link ClassWriter} instance.
	 */
	public ComponentWriterImpl() {
		super();
	}

	/**
	 * 
	 * @param out
	 *            the stream to print the class to.
	 */
	public ComponentWriterImpl(PrintStream out) {
		setWriter(new BasicClassWriter(out));
	}

	/**
	 * Adds extra imports necessary for this implementation.
	 */
	@Override
	public void printImports(Set<Class<?>> imports) {
		// add the imports we know we need
		imports.add(FacesContext.class);
		imports.add(ComponentHelp.class);
		imports.add(FacesEvent.class);

		super.printImports(imports);
	}

	/**
	 * @see ComponentWriter#printStringConstant(String, String)
	 */
	public void printStringConstant(String name, String value) {
		printConstant(String.class, name, value == null ? null : String.format(
				"\"%s\"", value));
	}

	/**
	 * @see ComponentWriter#printPropertyField(Class, String)
	 */
	public void printPropertyField(Class<?> type, String name) {
		String fieldName = getFieldName(name);
		startField(Modifier.PRIVATE, type, fieldName);
		println(";\n");
		stateToSave.put(name, type);
	}

	public void printConstructor() {
		startConstructor(Modifier.PUBLIC, null);
		indent().println("super();");
		indent().print("setRendererType(DEFAULT_RENDERER_TYPE);");
		endBlock();
	}

	public void printCoreConstants(String defaultRendererType,
			String componentFamily, String componentType) {
		printStringConstant("DEFAULT_RENDERER_TYPE", defaultRendererType);
		printStringConstant("COMPONENT_FAMILY", componentFamily);
		printStringConstant("COMPONENT_TYPE", componentType);
	}

	public void printFamilyGetter() {
		startMethod(Modifier.PUBLIC, String.class, "getFamily", null);
		indent();
		print("return COMPONENT_FAMILY;");
		endBlock();
	}

	/**
	 * Writes the getAttribute method.
	 */
	@Override
	public void close() {
		printGetAttribute();

		printBroadcast();

		super.close();
	}

	/**
	 * Override broadcast with a method that will call
	 * {@link AjaxContext#broadcast(FacesEvent)} first.
	 */
	@SuppressWarnings("serial")
	protected void printBroadcast() {
		startMethod(Modifier.PUBLIC, void.class, "broadcast",
				new LinkedHashMap<String, Class<?>>() {
					{
						put("event", FacesEvent.class);
					}
				});

		// ComponentHelp.broadcast(this, event);
		indent().printType(ComponentHelp.class);
		println(".broadcast(this, event);");

		indent().print("super.broadcast(event);").endBlock();
	}

	/**
	 * Writes out the helper method used by the getters.
	 */
	protected void printGetAttribute() {
		indent();
		printModifiers(Modifier.PROTECTED);
		// no way to signal starting template parameters, so we hard code this
		// method
		// signature
		print("<T> T getAttribute(T value, T defaultValue, String attribute) ")
				.startBlock();
		indent().print("return ").printType(ComponentHelp.class);
		print(".getAttribute(value, defaultValue, attribute, this, getFacesContext());");
		endBlock();
	}

	@SuppressWarnings("serial")
	public void printAccessors(final Class<?> type, String name,
			String defaultValue, String constantName, boolean defaultSaved) {
		String capName = Utils.capitalize(name);
		final String fieldName = getFieldName(name);

		// write the getter
		startMethod(Modifier.PUBLIC, type, getGet(type) + capName, null);		
		indent().printf("return %sgetAttribute(%2$s, %3$s, %4$s);",
				(defaultSaved ? fieldName + " = " : ""), fieldName,
				defaultValue, constantName).endBlock();

		// write the setter
		startMethod(Modifier.PUBLIC, void.class, "set" + capName,
				new LinkedHashMap<String, Class<?>>() {
					{
						put(fieldName, type);
					}
				});
		indent().printf("this.%s = %1$s;", fieldName).endBlock();
	}

	private String getGet(Class<?> class1) {
		return boolean.class.equals(class1) ? "is" : "get";
	}

	@SuppressWarnings("serial")
	public void printSaveAndRestore(String... excludeFields) {
		// write save state
		startMethod(Modifier.PUBLIC, Object.class, "saveState",
				new LinkedHashMap<String, Class<?>>() {
					{
						put("context", FacesContext.class);
					}
				});

		indent().printf("Object[] state = new Object[%d];\n",
				stateToSave.size() + 1);
		indent().println("state[0] = super.saveState(context);");

		Set<String> ex = Utils.asSet(excludeFields);

		int i = 1;
		for (String name : stateToSave.keySet()) {
			if (!ex.contains(name)) {
				indent().printf("state[%d] = this.%s;\n", i++,
						getFieldName(name));
			}
		}
		indent().print("return state;").endBlock();

		// write restore state
		startMethod(Modifier.PUBLIC, void.class, "restoreState",
				new LinkedHashMap<String, Class<?>>() {
					{
						put("context", FacesContext.class);
						put("state", Object.class);
					}
				});
		indent().print("Object[] values = (Object[]) state;\n");
		indent().print("super.restoreState(context, values[0]);");

		i = 1;
		for (Entry<String, Class<?>> prop : stateToSave.entrySet()) {
			String name = prop.getKey();
			if (!ex.contains(name)) {
				Class<?> type = ClassUtils.box(prop.getValue());
				print('\n').indent();
				printf("this.%s = (", getFieldName(name));
				printType(type);
				printf(") values[%d];", i++);
			}
		}
		endBlock();
	}

	@SuppressWarnings("serial")
	public void printListenerCode(
			Iterable<Class<? extends FacesListener>> listeners) {
		for (final Class<? extends FacesListener> listener : listeners) {
			// write the add method
			startMethod(Modifier.PUBLIC, void.class, "add"
					+ listener.getSimpleName(),
					new LinkedHashMap<String, Class<?>>() {
						{
							put("listener", listener);
						}
					});
			indent().print("addFacesListener(listener);").endBlock();

			// write the getter
			Class<?> array = Array.newInstance(listener, 0).getClass();
			startMethod(Modifier.PUBLIC, array, "get"
					+ listener.getSimpleName() + 's', null);
			indent().print("return (").printType(array);
			print(") getFacesListeners(").printType(listener);
			print(".class);").endBlock();

			// write the remove method
			startMethod(Modifier.PUBLIC, void.class, "remove"
					+ listener.getSimpleName(),
					new LinkedHashMap<String, Class<?>>() {
						{
							put("listener", listener);
						}
					});
			indent().print("removeFacesListener(listener);").endBlock();
		}
	}

	/**
	 * 
	 * @return the fields (and their classes) that will be saved in saveState.
	 *         The original field name is saved as the key.
	 */
	public LinkedHashMap<String, Class<?>> getStateToSave() {
		return stateToSave;
	}

	/**
	 * Intended primarily for unit testing, you can override fields are saved in
	 * saveState (and restored in restoreState).
	 * 
	 * @param stateToSave
	 */
	public void setStateToSave(LinkedHashMap<String, Class<?>> stateToSave) {
		this.stateToSave = stateToSave;
	}

}
